diff --git a/keyserver/src/fetchers/policy-acknowledgment-fetchers.js b/keyserver/src/fetchers/policy-acknowledgment-fetchers.js index 50840c6ba..c9d02361d 100644 --- a/keyserver/src/fetchers/policy-acknowledgment-fetchers.js +++ b/keyserver/src/fetchers/policy-acknowledgment-fetchers.js @@ -1,38 +1,53 @@ // @flow import type { PolicyType } from 'lib/facts/policies.js'; import { type UserPolicyConfirmationType } from 'lib/types/policy-types.js'; import { dbQuery, SQL } from '../database/database.js'; async function fetchPolicyAcknowledgments( userID: string, policies: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const query = SQL` SELECT policy, confirmed FROM policy_acknowledgments WHERE user=${userID} AND policy IN (${policies}) `; const [data] = await dbQuery(query); return data; } async function fetchNotAcknowledgedPolicies( userID: string, policies: $ReadOnlyArray, ): Promise<$ReadOnlyArray> { const viewerAcknowledgments = await fetchPolicyAcknowledgments( userID, policies, ); return policies.filter(policy => { const policyAcknowledgment = viewerAcknowledgments.find( viewerAcknowledgment => viewerAcknowledgment.policy === policy, ); return !policyAcknowledgment?.confirmed; }); } -export { fetchPolicyAcknowledgments, fetchNotAcknowledgedPolicies }; +async function hasAnyNotAcknowledgedPolicies( + userID: string, + policies: $ReadOnlyArray, +): Promise { + const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( + userID, + policies, + ); + return !!notAcknowledgedPolicies.length; +} + +export { + fetchPolicyAcknowledgments, + fetchNotAcknowledgedPolicies, + hasAnyNotAcknowledgedPolicies, +}; diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index 3b47a06d9..de893ff6e 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,360 +1,402 @@ // @flow import html from 'common-tags/lib/html'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import _keyBy from 'lodash/fp/keyBy'; import * as React from 'react'; import ReactDOMServer from 'react-dom/server'; import { promisify } from 'util'; +import { baseLegalPolicies } from 'lib/facts/policies.js'; import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer'; import { freshMessageStore } from 'lib/reducers/message-reducer'; import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors'; import { mostRecentMessageTimestamp } from 'lib/shared/message-utils'; import { threadHasPermission, threadIsPending, parsePendingThreadID, createPendingThread, } from 'lib/shared/thread-utils'; import { defaultWebEnabledApps } from 'lib/types/enabled-apps'; import { defaultCalendarFilters } from 'lib/types/filter-types'; import { defaultNumberPerThread } from 'lib/types/message-types'; import { defaultEnabledReports } from 'lib/types/report-types'; import { defaultConnectionInfo } from 'lib/types/socket-types'; import { threadPermissions, threadTypes } from 'lib/types/thread-types'; import { currentDateInTimeZone } from 'lib/utils/date-utils'; import { ServerError } from 'lib/utils/errors'; import { promiseAll } from 'lib/utils/promises'; import getTitle from 'web/title/getTitle'; import { navInfoFromURL } from 'web/url-utils'; import { fetchEntryInfos } from '../fetchers/entry-fetchers'; import { fetchMessageInfos } from '../fetchers/message-fetchers'; +import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { fetchThreadInfos } from '../fetchers/thread-fetchers'; import { fetchCurrentUserInfo, fetchKnownUserInfos, } from '../fetchers/user-fetchers'; import { setNewSession } from '../session/cookies'; import { Viewer } from '../session/viewer'; import { streamJSON, waitForStream } from '../utils/json-stream'; import { getAppURLFactsFromRequestURL } from '../utils/urls'; const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { jsURL: string, fontsURL: string, cssInclude: string }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', }; return assetInfo; } try { const assetsString = await readFile('../web/dist/assets.json', 'utf8'); const assets = JSON.parse(assetsString); assetInfo = { jsURL: `compiled/${assets.browser.js}`, fontsURL: googleFontsURL, cssInclude: html` `, }; return assetInfo; } catch { throw new Error( 'Could not load assets.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.default.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); const { basePath, baseDomain } = appURLFacts; const baseURL = basePath.replace(/\/$/, ''); const baseHref = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); + const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( + viewer.id, + baseLegalPolicies, + ); let initialNavInfo; try { initialNavInfo = navInfoFromURL(req.url, { now: currentDateInTimeZone(viewer.timeZone), }); } catch (e) { throw new ServerError(e.message); } const calendarQuery = { startDate: initialNavInfo.startDate, endDate: initialNavInfo.endDate, filters: defaultCalendarFilters, }; const messageSelectionCriteria = { joinedThreads: true }; const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); const threadInfoPromise = fetchThreadInfos(viewer); const messageInfoPromise = fetchMessageInfos( viewer, messageSelectionCriteria, defaultNumberPerThread, ); const entryInfoPromise = fetchEntryInfos(viewer, [calendarQuery]); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { if (viewer.loggedIn) { await setNewSession(viewer, calendarQuery, initialTime); } return viewer.sessionID; })(); const threadStorePromise = (async () => { - const { threadInfos } = await threadInfoPromise; - return { threadInfos }; + const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ + threadInfoPromise, + hasNotAcknowledgedPoliciesPromise, + ]); + return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; })(); const messageStorePromise = (async () => { const [ { threadInfos }, { rawMessageInfos, truncationStatuses }, - ] = await Promise.all([threadInfoPromise, messageInfoPromise]); + hasNotAcknowledgedPolicies, + ] = await Promise.all([ + threadInfoPromise, + messageInfoPromise, + hasNotAcknowledgedPoliciesPromise, + ]); + if (hasNotAcknowledgedPolicies) { + return { + messages: {}, + threads: {}, + local: {}, + currentAsOf: 0, + }; + } const { messageStore: freshStore } = freshMessageStore( rawMessageInfos, truncationStatuses, mostRecentMessageTimestamp(rawMessageInfos, initialTime), threadInfos, ); return freshStore; })(); const entryStorePromise = (async () => { - const { rawEntryInfos } = await entryInfoPromise; + const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ + entryInfoPromise, + hasNotAcknowledgedPoliciesPromise, + ]); + if (hasNotAcknowledgedPolicies) { + return { + entryInfos: {}, + daysToEntries: {}, + lastUserInteractionCalendar: 0, + }; + } return { entryInfos: _keyBy('id')(rawEntryInfos), daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), lastUserInteractionCalendar: initialTime, }; })(); const userStorePromise = (async () => { - const userInfos = await userInfoPromise; - return { userInfos, inconsistencyReports: [] }; + const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ + userInfoPromise, + hasNotAcknowledgedPoliciesPromise, + ]); + return { + userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, + inconsistencyReports: [], + }; })(); const navInfoPromise = (async () => { const [ { threadInfos }, messageStore, currentUserInfo, userStore, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userStorePromise, ]); const finalNavInfo = initialNavInfo; const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; if ( requestedActiveChatThreadID && !threadIsPending(requestedActiveChatThreadID) && !threadHasPermission( threadInfos[requestedActiveChatThreadID], threadPermissions.VISIBLE, ) ) { finalNavInfo.activeChatThreadID = null; } if (!finalNavInfo.activeChatThreadID) { const mostRecentThread = mostRecentlyReadThread( messageStore, threadInfos, ); if (mostRecentThread) { finalNavInfo.activeChatThreadID = mostRecentThread; } } if ( finalNavInfo.activeChatThreadID && threadIsPending(finalNavInfo.activeChatThreadID) && finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID ) { const pendingThreadData = parsePendingThreadID( finalNavInfo.activeChatThreadID, ); if ( pendingThreadData && pendingThreadData.threadType !== threadTypes.SIDEBAR && currentUserInfo.id ) { const { userInfos } = userStore; const members = pendingThreadData.memberIDs .map(id => userInfos[id]) .filter(Boolean); const newPendingThread = createPendingThread({ viewerID: currentUserInfo.id, threadType: pendingThreadData.threadType, members, }); finalNavInfo.activeChatThreadID = newPendingThread.id; finalNavInfo.pendingThread = newPendingThread; } } return finalNavInfo; })(); + const currentAsOfPromise = (async () => { + const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; + return hasNotAcknowledgedPolicies ? 0 : initialTime; + })(); const { jsURL, fontsURL, cssInclude } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); res.write(html`
`); } export { websiteResponder };